{- This file is part of funbot.
 -
 - Written in 2015, 2016 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

{-# LANGUAGE OverloadedStrings #-}

module FunBot.Puppet
    ( puppetStart
    , puppetPrivateStart
    , puppetEnd
    , puppetPrivateEnd
    , puppetSay
    , puppetPrivateSay
    , puppetCheck
    , puppetCheckChannel
    )
where

import Control.Monad (when)
import Data.Maybe
import Data.Monoid ((<>))
import Data.Foldable (traverse_)
import Formatting ((%))
import FunBot.Types
import Network.IRC.Fun.Bot.Chat
import Network.IRC.Fun.Bot.State
import Network.IRC.Fun.Color.Format (formatMsg)
import Network.IRC.Fun.Color.Format.Long
import Network.IRC.Fun.Types.Base

import qualified Data.HashMap.Lazy as M
import qualified Data.HashSet as S

-- | Start puppet mode in the given channel by the given nickname. Return
-- 'Nothing' on success. Otherwise 'False' means the channel is already in
-- puppet mode, and 'True' means it isn't but the user isn't a puppeteer for
-- that channel.
puppetStart :: Channel -> Nickname -> BotSession (Maybe Bool)
puppetStart chan nick = do
    puppet <- getStateS bsPuppet
    if isJust $ M.lookup chan puppet
        then return $ Just False
        else do
            gpts <- getStateS $ stPuppeteers . bsSettings
            mcs <- getStateS $ M.lookup chan . stChannels . bsSettings
            let lpts = fromMaybe S.empty $ fmap csPuppeteers mcs
            if nick `S.member` gpts || nick `S.member` lpts
                then do
                    let puppet' = M.insert chan nick puppet
                    modifyState $ \ s -> s { bsPuppet = puppet' }
                    return Nothing
                else return $ Just True

-- | Start private puppet mode by the given nickname. Return 'Nothing' on
-- success. Otherwise 'False' means private puppet mode is already on, and
-- 'True' means it isn't but the user isn't a global puppeteer.
puppetPrivateStart :: Nickname -> BotSession (Maybe Bool)
puppetPrivateStart nick = do
    mpteer <- getStateS bsPrivPuppet
    if isJust mpteer
        then return $ Just False
        else do
            gpts <- getStateS $ stPuppeteers . bsSettings
            if nick `S.member` gpts
                then do
                    modifyState $ \ s -> s { bsPrivPuppet = Just nick }
                    return Nothing
                else return $ Just True

-- | Stop puppet mode in a channel. Return 'Nothing' on success. Otherwise
-- 'False' means the channel isn't in puppet mode, and 'True' means it is, but
-- the user isn't a puppeteer there.
--
-- Note that any puppeteer can stop puppet mode, not necessarily the one who
-- started it. This can be useful in case the latter forgets to stop it or gets
-- disconnected from IRC, and then someone else can do it.
puppetEnd :: Channel -> Nickname -> BotSession (Maybe Bool)
puppetEnd chan nick = do
    puppet <- getStateS bsPuppet
    if isJust $ M.lookup chan puppet
        then do
            gpts <- getStateS $ stPuppeteers . bsSettings
            mcs <- getStateS $ M.lookup chan . stChannels . bsSettings
            let lpts = fromMaybe S.empty $ fmap csPuppeteers mcs
            if nick `S.member` gpts || nick `S.member` lpts
                then do
                    let puppet' = M.delete chan puppet
                    modifyState $ \ s -> s { bsPuppet = puppet' }
                    return Nothing
                else return $ Just True
        else return $ Just False

-- | Stop private puppet mode. Return 'Nothing' on success. Otherwise
-- 'False' means private puppet mode is off, and 'True' means it's on, but
-- the user isn't a global puppeteer.
--
-- Note that any global puppeteer can stop private puppet mode, not necessarily
-- the one who started it. This can be useful in case the latter forgets to
-- stop it or gets disconnected from IRC, and then someone else can do it.
puppetPrivateEnd :: Nickname -> BotSession (Maybe Bool)
puppetPrivateEnd nick = do
    mpteer <- getStateS bsPrivPuppet
    if isJust mpteer
        then do
            gpts <- getStateS $ stPuppeteers . bsSettings
            if nick `S.member` gpts
                then do
                    modifyState $ \ s -> s { bsPrivPuppet = Nothing }
                    return Nothing
                else return $ Just True
        else return $ Just False

-- | While in puppet mode, ask the bot to send a message into the channel.
-- Return 'Nothing' on success (i.e. the message is sent to the IRC server).
-- Otherwise, 'False' means the channel isn't in puppet mode, and 'True' means
-- it is, but the user isn't the one who started it.
puppetSay
    :: Channel
    -> Nickname
    -> MsgContent
    -> Bool -- ^ Whether to reveal the message comes from the puppeteer
    -> BotSession (Maybe Bool)
puppetSay chan nick msg reveal = do
    puppet <- getStateS bsPuppet
    case M.lookup chan puppet of
        Nothing    -> return $ Just False
        Just pteer ->
            if nick == pteer
                then do
                    let msg' =
                            if reveal
                                then formatMsg
                                        ("[" % nickname % "] " % message)
                                        nick msg
                                else msg
                    sendToChannel chan msg'
                    return Nothing
                else return $ Just True

-- | While in private puppet mode, ask the bot to send a message to a user.
-- Return 'Nothing' on success (i.e. the message is sent to the IRC server).
-- Otherwise, 'False' means private puppet mode is off, and 'True' means
-- it is, but the user isn't the one who started it.
puppetPrivateSay
    :: Nickname -- ^ Recipient
    -> Nickname -- ^ Puppeteer
    -> MsgContent
    -> Bool     -- ^ Whether to reveal the message comes from the puppeteer
    -> BotSession (Maybe Bool)
puppetPrivateSay recip nick msg reveal = do
    mpteer <- getStateS bsPrivPuppet
    case mpteer of
        Nothing    -> return $ Just False
        Just pteer ->
            if nick == pteer
                then do
                    let msg' =
                            if reveal
                                then formatMsg
                                        ("[" % nickname % "] " % message)
                                        nick msg'
                                else msg
                    sendToUser recip msg'
                    return Nothing
                else return $ Just True

-- | Finish puppet mode in all channels. This can be useful for emergency etc.
-- Return whether succeeded, i.e. whether user is a global puppeteer.
puppetReset
    :: Nickname -- ^ Must be a global puppeteer
    -> Bool     -- ^ Whether to announce end of puppet mode
    -> BotSession Bool
puppetReset nick ann = do
    gpteers <- getStateS $ stPuppeteers . bsSettings
    if nick `S.member` gpteers
        then do
            puppetChans <- getStateS $ M.keys . bsPuppet
            muser <- getStateS bsPrivPuppet
            modifyState $
                \ s -> s { bsPuppet = M.empty, bsPrivPuppet = Nothing }
            when ann $ do
                let msg =
                        MsgContent $ "Puppet mode reset by " <> unNickname nick
                traverse_ (flip sendToChannel msg) puppetChans
                traverse_ (flip sendToUser msg) muser
            return True
        else return False

-- | Check in which channels puppet mode is enabled, and whether private puppet
-- mode is enabled.
puppetCheck :: BotSession ([Channel], Bool)
puppetCheck = do
    chans <- getStateS $ M.keys . bsPuppet
    priv <- getStateS $ isJust . bsPrivPuppet
    return (chans, priv)

-- | Check whether puppet mode is enabled in a given channel.
puppetCheckChannel :: Channel -> BotSession Bool
puppetCheckChannel chan = getStateS $ (chan `M.member`) . bsPuppet
